/**
 * Copyright (C) 2011 Ron Coleman. Contact: ronncoleman@gmail.com
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package bj.actor

import scala.actors.Actor
import bj.hkeeping._
import bj.card._
import scala.collection.mutable.HashMap
import scala.actors.OutputChannel
import bj.util.Logs
import bj.util.Config
import scala.actors.Actor._

case class Request
case class Hit(pid: Int) extends Request
case class Stay(pid: Int) extends Request
case class DoubleDown(pid: Int) extends Request
case class Split(pid: Int) extends Request
case class StopGame(pid: Int) extends Request
case class Hole(card: Card)
case class Up(card: Card)
case class Observe(card: Card, player: Int, shoeSize: Int)
case class DealerCard(card: Card, dealer: Int)
case class Outcome
case class Win(gain: Double) extends Outcome
case class Lose(gain: Double) extends Outcome
case class Push(gain: Double) extends Outcome
case class PlayerBlackJack
case class DealerBlackJack
case class BetterPlayerBlackJack
case class BotPlayerBlackJack(tid: Int)
case class BetterPlayerBroke
case class BotPlayerBroke(tid: Int)
case class DealerBroke(size: Int)
case class BotResult(tid: Int, bet: Double, bankroll: Double)
case class BotPlayerDetail(bet: Double, bankroll: Double)

/**
 * @author Vikram
 *
 */

/** This object implements the dealer static members */
object Dealer {
  /** Unique id counter */
  var id: Int = -1
}

/**
 * This class implements the dealer.
 * @param table The table I'm associated with.
 */
class Dealer extends Actor with Hand with Logs {
  /** Unique dealer id */
  Dealer.id += 1
  val did = Dealer.id

  /** Mailboxes of players   */
  var players = List[OutputChannel[Any]]()

  /** Mailbox for the table */
  var table: OutputChannel[Any] = null

  /** Copies of player hands */
  var hands = List[Object with Hand]()

  /** Hand-to-pid map */
  var handMap = HashMap[Object with Hand, Int]()

  /** Shoe from which cards are dealt */
  var shoe: Shoe = null

  /** Current player index */
  var playerIndex: Int = 0

  /** Current player index */
  var curPlayer: OutputChannel[Any] = null

  /** Current hand in play */
  var curHand: Object with Hand = _

  /** My hole (faced down) card */
  var hole: Card = null

  /** Shuffle Seed for shoe */
  var seed: Int = 1

  /** Starts the dealer actor */

  start

  /** Converts this instance to a string */
  override def toString: String = "dealer(" + did + ")"

  /** This method receives messages */
  def act {
    loop {
      react {
        /** Receives players at the table */
        case GameStart(players: List[OutputChannel[Any]]) =>
          debug(this + " received game start w. " + players.size + " player mailboxes")
          gameStart(players, sender)

        /** Receives hit request from a player */
        case Hit(pid) =>
          debug(this + " received HIT from player(" + pid + ")")

          hit(pid, sender)

        /** Receives a stay request from a player */
        case Stay(pid) =>
          debug(this + " received STAY from player(" + pid + ")")

          stay(pid, sender)
        /** Receives something completely from left field */

        /** Receives double down request from a player for a specific hand */
        case DoubleDown(pid) =>
          debug(this + " received DOUBLEDOWN from player(" + pid + ")")
          doubleDown(pid, sender)

        /** Receives seed from a player to create a shoe */
        case ShuffleSeed(seed) =>
          debug(this + " received Seed from player =" + seed)
          this.seed = seed

        case dontKnow =>
          /** Got something we REALLY didn't expect */
          debug(this + " got " + dontKnow)
      }
    }
  }

  /** Initializes the game */
  def init {

    if (shoe == null)
      shoe = new Shoe(seed)

    /** Deal the up card */
    hit(shoe.deal)

    /** Deal the hole card but don't put it yet into dealer's hand */
    hole = shoe.deal

    /**  Deal two cards to each player at the table */
    players.foreach(deal)

    /** It's the first player's turn */
    playerIndex = 0

    curPlayer = players(0)

    curHand = hands(0)

    /** If current player get Blackjack then inform that player */
    if (curHand.blackjack) {
      curPlayer ! PlayerBlackJack
    }

    curPlayer ! Up(cards(0))
  }

  /**
   * Starts the game.
   * @param players Player mailboxes of this game
   * @param source Source actor of the message
   */
  def gameStart(players: List[OutputChannel[Any]], source: OutputChannel[Any]) {
    table = source

    this.players = players

    init

  }

  /**
   * Requests hit for the source pid.
   * @param pid Player id
   * @param source Source actor of the message.
   */
  def hit(pid: Int, player: OutputChannel[Any]) {
    if (!valid(player))
      player ! NotOk

    else {
      val card = shoe.deal

      deal(player, card)

      curHand.hit(card)

      if (curHand.broke) {

        player ! Broke

        moveNext(pid)
      }
    }
  }

  /**
   * Requests stay for the source pid.
   * @param pid Player id
   * @param source Source actor of the message.
   */
  def stay(pid: Int, source: OutputChannel[Any]) {
    if (!valid(source))
      source ! NotOk

    source ! Ok

    moveNext(pid)
  }

  def doubleDown(pid: Int, player: OutputChannel[Any]) {

    /** hit the next card and deal it to the player */
    val card = shoe.deal
    deal(player, card)

    /** add the card to the dealer's copy of the hand and set the doubledown flag to true */
    curHand.doubled = true
    curHand.hit(card)

    /** if this card broke the player, let him know */
    if (curHand.broke) {

      player ! Broke
    }
    /** move on to the next hand */
    moveNext(pid)

  }

  /**
   * Moves to the next player at the table.
   * @param pid Player id
   */
  def moveNext(pid: Int) {

    Thread.sleep(Config.delayTime)

    /** Save the hand to pid map -- we need later for pay outs */
    handMap(curHand) = pid

    /** Advance the player index */
    playerIndex += 1

    /** If there are more players, send my up-card which signals the player that its turn begins */
    if (playerIndex < players.size) {
      curPlayer = players(playerIndex)

      curHand = hands(playerIndex)

      /** If current player get Blackjack then inform that player */
      if (curHand.blackjack) {
        curPlayer ! PlayerBlackJack
      }

      curPlayer ! Up(cards(0))

    } else {
      /** Close the game with dealer play */
      close

      /** Do the payouts */
      payout
    }

  }

  /** Closes out the game */
  def close {
    /** If no players are left, dealer doesn't have to play */
    if (playersLeft == 0)
      return

    /** Hit dealer's hand with the hole card */
    hit(hole)

    debug(this + " closing card1 = " + cards(0) + " card2 = " + cards(1))

    while (value < 17 && !blackjack) {
      val card: Card = shoe.deal

      hit(card)

      debug(this + " hitting card = " + card + " value = " + value)
    }
    /**
     * Broadcast observations on all cards dealer's holding
     *  except the up-card at index 0 -- players have already
     *  seen this card
     */
    for (i <- (0 until players.size)) {
      if (players(i) != null) {
        players(i) ! Hole(cards(1))
        for (j <- (2 until cards.size)) {
          players(i) ! DealerCard(cards(j), did)
        }
      }
    }

    /** If Dealer get Blackjack then inform all the players */
    if (blackjack) {
      players.foreach(p => p ! DealerBlackJack)
      debug("Dealer got BlackJack")
    }

    /** If Dealer get Broke then inform all the players */
    if (value > 21) {
      debug(this + " BROKE!!")
      players.foreach(p => p ! DealerBroke(curHand.cards.size))
    }
  }

  /** Returns count of players who haven't broken */
  def playersLeft = hands.count(h => !h.broke)

  /** Sends pay-out info to the table */
  def payout {
    val pays = HashMap[Int, Outcome]()

    for (hand <- hands) {
      val pid = handMap(hand)

      /** Player looses */
      if (hand.broke)
        pays(pid) = Lose(-1.0)

      else if (!this.broke && hand.value < this.value)
        pays(pid) = Lose(-1.0)

      /** Push */
      else if (this.value == hand.value)
        pays(pid) = Push(0.0)

      /** Player wins automatically with blackjack */
      else if (hand.blackjack)
        pays(pid) = Win(1.5)

      /** Player wins if hand value is greater */
      else if (hand.value > this.value)
        pays(pid) = Win(1.0)

      /** Player wins if dealer breaks */
      else if (!hand.broke && this.broke)
        pays(pid) = Win(1.0)
    }

    table ! GameOver(pays)

    clear
  }

  /**
   * Deals initial cards to a player
   * @param mailbox The player's mailbox
   */
  def deal(player: OutputChannel[Any]): Unit = {
    val card1 = shoe.deal
    val card2 = shoe.deal

    debug(this + " dealing " + card1)
    debug(this + " dealing " + card2)

    deal(player, card1)

    deal(player, card2)

    val hand = new Object with Hand

    hand.hit(card1)
    hand.hit(card2)

    hands = hands ::: List(hand)
  }

  /**
   * Sends the destination player a card.
   * @param target Mailbox of the destination player.
   * @param card Card to send
   */
  def deal(player: OutputChannel[Any], card: Card) {
    /** Send the card to the player */
    player ! card

    /** Broadcast the card to all observers */
    for (i <- (0 until players.size))
      if (players(i) != player)
        players(i) ! Observe(card, i, shoe.size)
  }

  /**
   * Clears the parameters
   * @param end of the Game
   */
  def clear: Unit = {

    players = List[OutputChannel[Any]]()
    playerIndex = 0
    hands = List[Object with Hand]()
    curHand = null
    handMap.clear
    cards = List[Card]()
    hole = null
  }

  /**
   * Validates a mailbox
   * @param mailbox The player's mailbox
   */
  def valid(player: OutputChannel[Any]): Boolean = {
    if (player == null)
      return false

    debug(this + " valid players sz = " + players.size)
    players.find(p => p == player) match {
      case None =>
        debug(this + " valid got none player")
        return false

      case Some(player) =>
        debug(this + " got some player " + player)
        if (player != curPlayer)
          return false

        return !curHand.broke
    }
  }
}